/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.search;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.lucene.search.FuzzyQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.tests.search.QueryUtils;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.SolrRequestInfo;
import org.apache.solr.response.SolrQueryResponse;
import org.junit.AfterClass;
import org.junit.BeforeClass;

/**
 * Sanity checks that queries (generated by the QParser and ValueSourceParser framework) are
 * appropriately {@link Object#equals} and {@link Object#hashCode()} equivalent. If you are adding a
 * new default QParser or ValueSourceParser, you will most likely get a failure from {@link
 * #testParserCoverage} until you add a new test method to this class.
 *
 * @see ValueSourceParser#standardValueSourceParsers
 * @see QParserPlugin#standardPlugins
 * @see QueryUtils
 */
public class QueryEqualityTest extends SolrTestCaseJ4 {

  @BeforeClass
  public static void beforeClass() throws Exception {
    initCore("solrconfig.xml", "schema15.xml");
  }

  /**
   * @see #testParserCoverage
   */
  @AfterClass
  public static void afterClassParserCoverageTest() {

    if (!doAssertParserCoverage) return;
    for (String name : QParserPlugin.standardPlugins.keySet()) {
      assertTrue(
          "testParserCoverage was run w/o any other method explicitly testing qparser: " + name,
          qParsersTested.contains(name));
    }

    for (final String name : ValueSourceParser.standardValueSourceParsers.keySet()) {
      assertTrue(
          "testParserCoverage was run w/o any other method explicitly testing val parser: " + name,
          valParsersTested.contains(name));
    }
  }

  /**
   * @see #testParserCoverage
   */
  private static boolean doAssertParserCoverage = false;

  /**
   * @see #testParserCoverage
   */
  private static final Set<String> qParsersTested = new HashSet<>();

  /**
   * @see #testParserCoverage
   */
  private static final Set<String> valParsersTested = new HashSet<>();

  public void testDateMathParsingEquality() throws Exception {
    // regardless of parser, these should all be equivalent queries
    assertQueryEquals(
        null,
        "{!lucene}f_tdt:2013-09-11T00\\:00\\:00Z",
        "{!lucene}f_tdt:2013-03-08T00\\:46\\:15Z/DAY+6MONTHS+3DAYS",
        "{!lucene}f_tdt:\"2013-03-08T00:46:15Z/DAY+6MONTHS+3DAYS\"",
        "{!field f=f_tdt}2013-03-08T00:46:15Z/DAY+6MONTHS+3DAYS",
        "{!field f=f_tdt}2013-09-11T00:00:00Z",
        "{!term f=f_tdt}2013-03-08T00:46:15Z/DAY+6MONTHS+3DAYS",
        "{!term f=f_tdt}2013-09-11T00:00:00Z");
  }

  public void testQueryLucene() throws Exception {
    assertQueryEquals(
        "lucene", "{!lucene}apache solr",
        "apache  solr", "apache solr ");
    assertQueryEquals("lucene", "+apache +solr", "apache AND solr", " +apache +solr");
  }

  public void testQueryLuceneAllDocsWithField() throws Exception {
    // for all "primitive" types except for doubles/floats, 'foo:*' should be functionally
    // equivalent to "foo:[* TO *]" whatever implementation/optimizations exist for one syntax,
    // should exist for the other syntax as well (regardless of docValues, multivalued, etc...)
    for (String field :
        Arrays.asList(
            "foo_sI",
            "foo_sS",
            "foo_s1",
            "foo_s",
            "t_foo",
            "tv_foo",
            "tv_mv_foo",
            "foo_b",
            "foo_b_dvo",
            "foo_i",
            "foo_is",
            "foo_i_dvo",
            "foo_l",
            "foo_l_dvo",
            "foo_dt",
            "foo_dt_dvo")) {

      assertQueryEquals("lucene", field + ":*", field + ":[* TO *]");
    }
  }

  public void testQueryPrefix() throws Exception {
    SolrQueryRequest req = req("myField", "foo_s");
    try {
      assertQueryEquals("prefix", req, "{!prefix f=$myField}asdf", "{!prefix f=foo_s}asdf");
    } finally {
      req.close();
    }
  }

  public void testQueryFuzzy() throws Exception {
    try (SolrQueryRequest req = req("myField", "foo_s")) {
      assertQueryEquals("fuzzy", req, "{!fuzzy f=$myField}asdf", "{!fuzzy f=foo_s}asdf");
      assertQueryEquals("fuzzy", req, "{!fuzzy f=$myField}asdf", "{!fuzzy f=foo_s v=asdf}");
      FuzzyQuery q =
          (FuzzyQuery)
              assertQueryEqualsAndReturn("fuzzy", req, "{!fuzzy f=$myField prefixLength=10}asdf");
      assertEquals(10, q.getPrefixLength());
      q =
          (FuzzyQuery)
              assertQueryEqualsAndReturn("fuzzy", req, "{!fuzzy f=$myField maxEdits=1}asdf");
      assertEquals(FuzzyQuery.defaultPrefixLength, q.getPrefixLength());
      assertEquals(1, q.getMaxEdits());
      q =
          (FuzzyQuery)
              assertQueryEqualsAndReturn(
                  "fuzzy", req, "{!fuzzy f=$myField maxExpansions=4 transpositions=false}asdf");
      assertFalse(q.getTranspositions());
    }
  }

  public void testQueryBoost() throws Exception {
    SolrQueryRequest req = req("df", "foo_s", "myBoost", "sum(3,foo_i)");
    try {
      assertQueryEquals(
          "boost",
          req,
          "{!boost b=$myBoost}asdf",
          "{!boost b=$myBoost v=asdf}",
          "{!boost b=sum(3,foo_i)}foo_s:asdf");
    } finally {
      req.close();
    }
  }

  public void testReRankQuery() throws Exception {
    final String defType = ReRankQParserPlugin.NAME;
    SolrQueryRequest req =
        req(
            "q", "*:*",
            "rqq", "{!edismax}hello",
            "rdocs", "20",
            "rweight", "2",
            "rows", "10",
            "start", "0");
    try {
      assertQueryEquals(
          defType,
          req,
          "{!"
              + defType
              + " "
              + ReRankQParserPlugin.RERANK_QUERY
              + "=$rqq "
              + ReRankQParserPlugin.RERANK_DOCS
              + "=$rdocs "
              + ReRankQParserPlugin.RERANK_WEIGHT
              + "=$rweight}",
          "{!"
              + defType
              + " "
              + ReRankQParserPlugin.RERANK_QUERY
              + "=$rqq "
              + ReRankQParserPlugin.RERANK_DOCS
              + "=20 "
              + ReRankQParserPlugin.RERANK_WEIGHT
              + "=2}");

    } finally {
      req.close();
    }

    req =
        req(
            "qq",
            "*:*",
            "rqq",
            "{!edismax}hello",
            "rdocs",
            "20",
            "rweight",
            "2",
            "rows",
            "100",
            "start",
            "50");
    try {
      assertQueryEquals(
          defType,
          req,
          "{!"
              + defType
              + " mainQuery=$qq "
              + ReRankQParserPlugin.RERANK_QUERY
              + "=$rqq "
              + ReRankQParserPlugin.RERANK_DOCS
              + "=$rdocs "
              + ReRankQParserPlugin.RERANK_WEIGHT
              + "=$rweight}",
          "{!"
              + defType
              + " mainQuery=$qq "
              + ReRankQParserPlugin.RERANK_QUERY
              + "=$rqq "
              + ReRankQParserPlugin.RERANK_DOCS
              + "=20 "
              + ReRankQParserPlugin.RERANK_WEIGHT
              + "=2}");

    } finally {
      req.close();
    }
  }

  public void testExportQuery() throws Exception {
    SolrQueryRequest req = req("q", "*:*");
    try {
      assertQueryEquals("xport", req, "{!xport}");
    } finally {
      req.close();
    }
  }

  public void testGraphTermsQuery() throws Exception {
    SolrQueryRequest req = req("q", "*:*");
    try {
      assertQueryEquals("graphTerms", req, "{!graphTerms f=field1_s maxDocFreq=1000}term1,term2");
    } finally {
      req.close();
    }
  }

  public void testTlogitQuery() throws Exception {
    SolrQueryRequest req =
        req(
            "q",
            "*:*",
            "feature",
            "f",
            "terms",
            "a,b,c",
            "weights",
            "100,200,300",
            "idfs",
            "1,5,7",
            "iteration",
            "1",
            "outcome",
            "a",
            "positiveLabel",
            "1");
    try {
      assertQueryEquals("tlogit", req, "{!tlogit}");
    } finally {
      req.close();
    }
  }

  public void testIGainQuery() throws Exception {
    SolrQueryRequest req =
        req("q", "*:*", "outcome", "b", "positiveLabel", "1", "field", "x", "numTerms", "200");
    try {
      assertQueryEquals("igain", req, "{!igain}");
    } finally {
      req.close();
    }
  }

  public void testSignificantTermsQuery() throws Exception {
    SolrQueryRequest req = req("q", "*:*");
    try {
      assertQueryEquals(
          SignificantTermsQParserPlugin.NAME, req, "{!" + SignificantTermsQParserPlugin.NAME + "}");
    } finally {
      req.close();
    }
  }

  public void testQuerySwitch() throws Exception {
    SolrQueryRequest req =
        req(
            "myXXX", "XXX",
            "myField", "foo_s",
            "myQ", "{!prefix f=$myField}asdf");
    try {
      assertQueryEquals(
          "switch",
          req,
          "{!switch case.foo=XXX case.bar=zzz case.yak=qqq}foo",
          "{!switch case.foo=qqq case.bar=XXX case.yak=zzz} bar ",
          "{!switch case.foo=qqq case.bar=XXX case.yak=zzz v='  bar '}",
          "{!switch default=XXX case.foo=qqq case.bar=zzz}asdf",
          "{!switch default=$myXXX case.foo=qqq case.bar=zzz}asdf",
          "{!switch case=XXX case.bar=zzz case.yak=qqq v=''}",
          "{!switch case.bar=zzz case=XXX case.yak=qqq v=''}",
          "{!switch case=XXX case.bar=zzz case.yak=qqq}",
          "{!switch case=XXX case.bar=zzz case.yak=qqq}   ",
          "{!switch case=$myXXX case.bar=zzz case.yak=qqq}   ");

      assertQueryEquals(
          "switch", req, "{!switch case.foo=$myQ case.bar=zzz case.yak=qqq}foo", "{!query v=$myQ}");
    } finally {
      req.close();
    }
  }

  public void testMatchAllDocsQueryXmlParser() throws Exception {
    final String type = "xmlparser";
    assertQueryEquals(
        type,
        "{!" + type + "}<MatchAllDocsQuery/>",
        "<MatchAllDocsQuery/>",
        "<MatchAllDocsQuery></MatchAllDocsQuery>");
  }

  public void testQueryDismax() throws Exception {
    for (final String type : new String[] {"dismax", "edismax"}) {
      assertQueryEquals(
          type, "{!" + type + "}apache solr", "apache solr", "apache  solr", "apache solr ");
      assertQueryEquals(type, "+apache +solr", "apache AND solr", " +apache +solr");
    }
  }

  public void testField() throws Exception {
    SolrQueryRequest req = req("myField", "foo_s");
    try {
      assertQueryEquals(
          "field",
          req,
          "{!field f=$myField}asdf",
          "{!field f=$myField v=asdf}",
          "{!field f=foo_s}asdf");
    } finally {
      req.close();
    }
  }

  public void testQueryRaw() throws Exception {
    SolrQueryRequest req = req("myField", "foo_s");
    try {
      assertQueryEquals(
          "raw", req, "{!raw f=$myField}asdf", "{!raw f=$myField v=asdf}", "{!raw f=foo_s}asdf");
    } finally {
      req.close();
    }
  }

  public void testQueryTerm() throws Exception {
    SolrQueryRequest req = req("myField", "foo_s");
    try {
      assertQueryEquals(
          "term",
          req,
          "{!term f=$myField}asdf",
          "{!term f=$myField v=asdf}",
          "{!term f=foo_s}asdf");
    } finally {
      req.close();
    }
  }

  @SuppressWarnings({"unchecked"})
  public void testQueryCollapse() throws Exception {
    SolrQueryRequest req =
        req(
            "myField", "foo_s1",
            "g_sort", "foo_s1 asc, foo_i desc");

    try {
      assertQueryEquals("collapse", req, "{!collapse field=$myField}");

      assertQueryEquals("collapse", req, "{!collapse field=$myField max=a}");

      assertQueryEquals(
          "collapse",
          req,
          "{!collapse field=$myField min=a}",
          "{!collapse field=$myField min=a nullPolicy=ignore}");

      assertQueryEquals(
          "collapse",
          req,
          "{!collapse field=$myField sort=$g_sort}",
          "{!collapse field=$myField sort='foo_s1 asc, foo_i desc'}",
          "{!collapse field=$myField sort=$g_sort nullPolicy=ignore}");

      assertQueryEquals("collapse", req, "{!collapse field=$myField max=a nullPolicy=expand}");

      // Add boosted documents to the request context.
      @SuppressWarnings({"rawtypes"})
      Map context = req.getContext();
      @SuppressWarnings({"rawtypes"})
      Set boosted = new HashSet();
      boosted.add("doc1");
      boosted.add("doc2");
      context.put("BOOSTED", boosted);

      assertQueryEquals(
          "collapse",
          req,
          "{!collapse field=$myField min=a}",
          "{!collapse field=$myField min=a nullPolicy=ignore}");

    } finally {
      req.close();
    }
  }

  public void testHash() throws Exception {
    SolrQueryRequest req = req("partitionKeys", "foo_s");

    try {
      assertQueryEquals("hash", req, "{!hash workers=3 worker=0}");

    } finally {
      req.close();
    }
  }

  public void testMinHash() throws Exception {
    SolrQueryRequest req = req("q", "apache lucene is a search library", "df", "min_hash_analyzed");

    try {
      assertQueryEquals(
          "min_hash",
          req,
          "{!min_hash field=\"min_hash_analysed\"}apache lucene is a search library");
    } finally {
      req.close();
    }
  }

  public void testRankQuery() throws Exception {
    SolrQueryRequest req = req("df", "foo_s");
    try {
      assertQueryEquals(
          "rank",
          req,
          "{!rank f='rank_1'}",
          "{!rank f='rank_1' function='satu'}",
          "{!rank f='rank_1' function='satu' weight=1}");
    } finally {
      req.close();
    }
  }

  public void testQueryNested() throws Exception {
    SolrQueryRequest req = req("df", "foo_s");
    try {
      assertQueryEquals(
          "query",
          req,
          "{!query defType=lucene}asdf",
          "{!query v='foo_s:asdf'}",
          "{!query}foo_s:asdf",
          "{!query}asdf");
    } finally {
      req.close();
    }
  }

  public void testQueryFunc() throws Exception {
    // more involved tests of specific functions in other methods
    SolrQueryRequest req =
        req(
            "myVar", "5",
            "myField", "foo_i",
            "myInner", "product(4,foo_i)");
    try {
      assertQueryEquals("func", req, "{!func}sum(4,5)", "{!func}sum(4,$myVar)", "sum(4,5)");
      assertQueryEquals(
          "func", req, "{!func}sum(1,2,3,4,5)", "{!func}sum(1,2,3,4,$myVar)", "sum(1,2,3,4,5)");
      assertQueryEquals(
          "func",
          req,
          "{!func}sum(4,$myInner)",
          "{!func}sum(4,product(4,foo_i))",
          "{!func}sum(4,product(4,$myField))",
          "{!func}sum(4,product(4,field(foo_i)))");
    } finally {
      req.close();
    }
  }

  public void testQueryFrange() throws Exception {
    SolrQueryRequest req =
        req(
            "myVar", "5",
            "low", "0.2",
            "high", "20.4",
            "myField", "foo_i",
            "myInner", "product(4,foo_i)");
    try {
      // NOTE: unlike most queries, frange defaults to cost==100
      assertQueryEquals(
          "frange",
          req,
          "{!frange l=0.2 h=20.4}sum(4,5)",
          "{!frange l=0.2 h=20.4 cost=100}sum(4,5)",
          "{!frange l=$low h=$high}sum(4,$myVar)");
    } finally {
      req.close();
    }
  }

  public void testQueryGeofilt() throws Exception {
    checkQuerySpatial("geofilt");
  }

  public void testQueryBbox() throws Exception {
    checkQuerySpatial("bbox");
  }

  public void testLocalParamsWithRepeatingParam() throws Exception {
    SolrQueryRequest req =
        req(
            "q", "foo",
            "bq", "111",
            "bq", "222");
    try {
      assertQueryEquals(
          "dismax",
          req,
          "{!dismax}foo",
          "{!dismax bq=111 bq=222}foo",
          "{!dismax bq=222 bq=111}foo");
    } finally {
      req.close();
    }
  }

  private void checkQuerySpatial(final String type) throws Exception {
    SolrQueryRequest req =
        req(
            "myVar", "5",
            "d", "109",
            "pt", "10.312,-20.556",
            "sfield", "store");
    try {
      assertQueryEquals(
          type,
          req,
          "{!" + type + " d=109}",
          "{!" + type + " sfield=$sfield}",
          "{!" + type + " sfield=store d=109}",
          "{!" + type + " sfield=store d=$d pt=$pt}",
          "{!" + type + " sfield=store d=$d pt=10.312,-20.556}",
          "{!" + type + "}");
      // diff SpatialQueryable FieldTypes matter for determining final query
      assertQueryEquals(
          type,
          req,
          "{!" + type + " sfield=xy}",
          "{!" + type + " sfield=xy d=109}",
          "{!" + type + " sfield=xy d=$d pt=$pt}",
          "{!" + type + " sfield=xy d=$d pt=10.312,-20.556}");
    } finally {
      req.close();
    }
  }

  public void testQueryJoin() throws Exception {
    SolrQueryRequest req =
        req(
            "myVar", "5",
            "df", "text",
            "ff", "foo_s",
            "tt", "bar_s");

    try {
      assertQueryEquals(
          "join",
          req,
          "{!join from=foo_s to=bar_s}asdf",
          "{!join from=$ff to=$tt}asdf",
          "{!join from=$ff to='bar_s'}text:asdf");
    } finally {
      req.close();
    }
  }

  public void testQueryScoreJoin() throws Exception {
    SolrQueryRequest req =
        req("myVar", "5", "df", "text", "ff", "foo_s", "tt", "bar_s", "scoreavg", "avg");

    try {
      assertQueryEquals(
          "join",
          req,
          "{!join from=foo_s to=bar_s score=avg}asdf",
          "{!join from=$ff to=$tt score=Avg}asdf",
          "{!join from=$ff to='bar_s' score=$scoreavg}text:asdf");
    } finally {
      req.close();
    }
  }

  public void testTerms() throws Exception {
    assertQueryEquals(
        "terms", "{!terms f=foo_i}10,20,30,-10,-20,-30", "{!terms f=foo_i}10,20,30,-10,-20,-30");
  }

  public void testBlockJoin() throws Exception {
    assertQueryEquals(
        "parent", "{!parent which=foo_s:parent}dude", "{!parent which=foo_s:parent}dude");
    assertQueryEquals("child", "{!child of=foo_s:parent}dude", "{!child of=foo_s:parent}dude");
    // zero query case
    assertQueryEquals(null, "{!parent which=foo_s:parent}", "{!parent which=foo_s:parent}");
    assertQueryEquals(null, "{!child of=foo_s:parent}", "{!child of=foo_s:parent}");
    assertQueryEquals(null, "{!parent which='+*:* -foo_s:parent'}", "{!child of=foo_s:parent}");

    try (SolrQueryRequest req =
        req(
            "fq",
            "bar_s:baz",
            "fq",
            "{!tag=fqban}bar_s:ban",
            "ffq",
            "bar_s:baz",
            "ffq",
            "{!tag=ffqban}bar_s:ban")) {
      assertQueryEquals(
          "filters",
          req,
          "{!parent which=foo_s:parent param=$fq}foo_s:bar",
          "{!parent which=foo_s:parent param=$ffq}foo_s:bar" // differently named params
          );
      assertQueryEquals(
          "filters",
          req,
          "{!parent which=foo_s:parent param=$fq excludeTags=fqban}foo_s:bar",
          "{!parent which=foo_s:parent param=$ffq excludeTags=ffqban}foo_s:bar" // differently named
          // params
          );

      QueryUtils.checkUnequal( // parent filter is not an equal to child
          QParser.getParser("{!child of=foo_s:parent}", req).getQuery(),
          QParser.getParser("{!parent which=foo_s:parent}", req).getQuery());
    }

    // check multiple ways of specifying _nest_path_ prefixes
    final String parent_path = "/aa/bb";
    try (SolrQueryRequest req =
        req(
            "parent_filt", "(*:* -{!prefix f='_nest_path_' v='" + parent_path + "/'})",
            "child_q", "(+foo +{!prefix f='_nest_path_' v='" + parent_path + "/'})",
            "parent_q", "(+bar +{!field f='_nest_path_' v='" + parent_path + "'})")) {

      assertQueryEquals(
          "parent",
          req,

          // using local params to refer to other query params using 'prefix' parser...
          "{!parent which=$parent_filt v=$child_q}",

          // using 'inline' prefix query syntax...
          //
          // '/' has to be escaped otherwise it will be treated as a regex query...
          // ...and when used inside the 'which' param it has to be escaped *AGAIN* because of
          // the "quoted" localparam evaluation layer...
          // (and of course '\' escaping is the java syntax as well, we have to double it)
          "{!parent which='*:* -_nest_path_:"
              + (parent_path + "/").replace("/", "\\\\/")
              + "*'}"
              + "(+foo +_nest_path_:"
              + (parent_path + "/").replace("/", "\\/")
              + "*)");

      assertQueryEquals(
          "child",
          req,

          // using local params to refer to other query params using 'prefix' parser...
          "{!child of=$parent_filt v=$parent_q}",

          // using 'inline' prefix query syntax...
          //
          // '/' has to be escaped other wise it will be treated as a regex query...
          // ...and when used inside the 'which' param it has to be escaped *AGAIN* because of
          // the "quoted" localparam evaluation layer...
          // (and of course '\' escaping is the java syntax as well, we have to double it)
          "{!child of='*:* -_nest_path_:"
              + (parent_path + "/").replace("/", "\\\\/")
              + "*'}"
              + "(+bar +_nest_path_:"
              + parent_path.replace("/", "\\/")
              + ")");
    }
  }

  public void testFilters() throws Exception {
    final SolrQueryRequest req =
        req(
            "fq",
            "bar_s:baz",
            "fq",
            "{!tag=fqban}bar_s:ban",
            "ffq",
            "{!tag=ffqbaz}bar_s:baz",
            "ffq",
            "{!tag=ffqban}bar_s:ban");
    try {
      assertQueryEquals(
          "filters",
          req,
          "{!filters param=$fq}foo_s:bar",
          "{!filters param=$fq}foo_s:bar",
          "{!filters param=$ffq}foo_s:bar" // differently named params
          );
      assertQueryEquals(
          "filters",
          req,
          "{!filters param=$fq excludeTags=fqban}foo_s:bar",
          "{!filters param=$ffq  excludeTags=ffqban}foo_s:bar");
      assertQueryEquals(
          "filters",
          req,
          "{!filters excludeTags=top}{!tag=top v='foo_s:bar'}",
          "{!filters param=$ffq excludeTags='ffqban,ffqbaz'}");
      QueryUtils.checkUnequal(
          QParser.getParser("{!filters param=$fq}foo_s:bar", req).getQuery(),
          QParser.getParser("{!filters param=$fq excludeTags=fqban}foo_s:bar", req).getQuery());
    } finally {
      req.close();
    }
  }

  public void testGraphQuery() throws Exception {
    SolrQueryRequest req =
        req(
            "from",
            "node_s",
            "to",
            "edge_s",
            "traversalFilter",
            "foo",
            "returnOnlyLeaf",
            "true",
            "returnRoot",
            "false",
            "maxDepth",
            "2",
            "useAutn",
            "false");
    // make sure all param substitution works for all args to graph query.
    assertQueryEquals(
        "graph", req, "{!graph from=node_s to=edge_s}*:*", "{!graph from=$from to=$to}*:*");

    assertQueryEquals(
        "graph",
        req,
        "{!graph from=node_s to=edge_s traversalFilter=foo}*:*",
        "{!graph from=$from to=$to traversalFilter=$traversalFilter}*:*");

    assertQueryEquals(
        "graph",
        req,
        "{!graph from=node_s to=edge_s traversalFilter=foo returnOnlyLeaf=true}*:*",
        "{!graph from=$from to=$to traversalFilter=$traversalFilter returnOnlyLeaf=$returnOnlyLeaf}*:*");

    assertQueryEquals(
        "graph",
        req,
        "{!graph from=node_s to=edge_s traversalFilter=foo returnOnlyLeaf=true returnRoot=false}*:*",
        "{!graph from=$from to=$to traversalFilter=$traversalFilter returnOnlyLeaf=$returnOnlyLeaf returnRoot=$returnRoot}*:*");

    assertQueryEquals(
        "graph",
        req,
        "{!graph from=node_s to=edge_s traversalFilter=foo returnOnlyLeaf=true returnRoot=false maxDepth=2}*:*",
        "{!graph from=$from to=$to traversalFilter=$traversalFilter returnOnlyLeaf=$returnOnlyLeaf returnRoot=$returnRoot maxDepth=$maxDepth}*:*");

    assertQueryEquals(
        "graph",
        req,
        "{!graph from=node_s to=edge_s traversalFilter=foo returnOnlyLeaf=true returnRoot=false maxDepth=2 useAutn=false}*:*",
        "{!graph from=$from to=$to traversalFilter=$traversalFilter returnOnlyLeaf=$returnOnlyLeaf returnRoot=$returnRoot maxDepth=$maxDepth useAutn=$useAutn}*:*");
  }

  public void testQuerySurround() throws Exception {
    assertQueryEquals(
        "surround", "{!surround}and(apache,solr)",
        "and(apache,solr)", "apache AND solr");
  }

  public void testQueryComplexPhrase() throws Exception {
    assertQueryEquals(
        "complexphrase", "{!complexphrase df=text}\"jo* smith\"", "text:\"jo* smith\"");
    assertQueryEquals(
        "complexphrase", "{!complexphrase df=title}\"jo* smith\"", "title:\"jo* smith\"");
  }

  public void testFuncTestfunc() throws Exception {
    assertFuncEquals("testfunc(foo_i)", "testfunc(field(foo_i))");
    assertFuncEquals("testfunc(23)");
    assertFuncEquals("testfunc(sum(23,foo_i))", "testfunc(sum(23,field(foo_i)))");
  }

  public void testFuncOrd() throws Exception {
    assertFuncEquals("ord(foo_s)", "ord(foo_s    )");
  }

  public void testFuncLiteral() throws Exception {
    SolrQueryRequest req = req("someVar", "a string");
    try {
      assertFuncEquals(req, "literal('a string')", "literal(\"a string\")", "literal($someVar)");
    } finally {
      req.close();
    }
  }

  public void testFuncRord() throws Exception {
    assertFuncEquals("rord(foo_s)", "rord(foo_s    )");
  }

  public void testFuncCscore() throws Exception {
    assertFuncEquals("cscore()", "cscore(  )");
  }

  public void testFuncTop() throws Exception {
    assertFuncEquals("top(sum(3,foo_i))");
  }

  public void testFuncLinear() throws Exception {
    SolrQueryRequest req = req("someVar", "27");
    try {
      assertFuncEquals(req, "linear(foo_i,$someVar,42)", "linear(foo_i,   27,   42)");
    } finally {
      req.close();
    }
  }

  public void testFuncRecip() throws Exception {
    SolrQueryRequest req = req("someVar", "27");
    try {
      assertFuncEquals(
          req, "recip(foo_i,$someVar,42,   27   )", "recip(foo_i,   27,   42,$someVar)");
    } finally {
      req.close();
    }
  }

  public void testFuncScale() throws Exception {
    SolrQueryRequest req = req("someVar", "27");
    try {
      assertFuncEquals(req, "scale(field(foo_i),$someVar,42)", "scale(foo_i, 27, 42)");
    } finally {
      req.close();
    }
  }

  public void testFuncDiv() throws Exception {
    assertFuncEquals("div(5,4)", "div(5, 4)");
    assertFuncEquals("div(foo_i,4)", "div(foo_i, 4)", "div(field('foo_i'), 4)");
    assertFuncEquals("div(foo_i,sub(4,field('bar_i')))", "div(field(foo_i), sub(4,bar_i))");
  }

  public void testFuncMod() throws Exception {
    assertFuncEquals("mod(5,4)", "mod(5, 4)");
    assertFuncEquals("mod(foo_i,4)", "mod(foo_i, 4)", "mod(field('foo_i'), 4)");
    assertFuncEquals("mod(foo_i,sub(4,field('bar_i')))", "mod(field(foo_i), sub(4,bar_i))");
  }

  public void testFuncMap() throws Exception {
    assertFuncEquals("map(field(foo_i), 0, 45, 100)", "map(foo_i, 0.0, 45, 100)");
  }

  public void testFuncSum() throws Exception {
    assertFuncEquals("sum(5,4)", "add(5, 4)");
    assertFuncEquals("sum(5,4,3,2,1)", "add(5, 4, 3, 2, 1)");
    assertFuncEquals("sum(foo_i,4)", "sum(foo_i, 4)", "sum(field('foo_i'), 4)");
    assertFuncEquals("add(foo_i,sub(4,field('bar_i')))", "sum(field(foo_i), sub(4,bar_i))");
  }

  public void testFuncProduct() throws Exception {
    assertFuncEquals("product(5,4,3,2,1)", "mul(5, 4, 3, 2, 1)");
    assertFuncEquals("product(5,4)", "mul(5, 4)");
    assertFuncEquals("product(foo_i,4)", "product(foo_i, 4)", "product(field('foo_i'), 4)");
    assertFuncEquals("mul(foo_i,sub(4,field('bar_i')))", "product(field(foo_i), sub(4,bar_i))");
  }

  public void testFuncSub() throws Exception {
    assertFuncEquals("sub(5,4)", "sub(5, 4)");
    assertFuncEquals("sub(foo_i,4)", "sub(foo_i, 4)");
    assertFuncEquals("sub(foo_i,sum(4,bar_i))", "sub(foo_i, sum(4,bar_i))");
  }

  public void testFuncVector() throws Exception {
    assertFuncEquals("vector(5,4, field(foo_i))", "vector(5, 4, foo_i)");
    assertFuncEquals("vector(foo_i,4)", "vector(foo_i, 4)");
    assertFuncEquals("vector(foo_i,sum(4,bar_i))", "vector(foo_i, sum(4,bar_i))");
  }

  public void testFuncKnnVector() throws Exception {
    try (SolrQueryRequest req =
        req(
            "v1", "[1,2,3]",
            "v2", " [1,2,3] ",
            "v3", " [1, 2, 3.0] ")) {
      assertFuncEquals(
          req,
          "vectorSimilarity(FLOAT32,COSINE,[1,2,3],[4,5,6])",
          "vectorSimilarity(FLOAT32, COSINE, [1, 2, 3], [4, 5, 6])",
          "vectorSimilarity(FLOAT32, COSINE,$v1, [4, 5, 6])",
          "vectorSimilarity(FLOAT32, COSINE, $v2 , [4, 5, 6])",
          "vectorSimilarity(FLOAT32, COSINE, $v3 , [4, 5, 6])");
    }

    try (SolrQueryRequest req =
        req(
            "f1", "bar_i",
            "f2", " bar_i ",
            "f3", " field(bar_i) ")) {
      assertFuncEquals(
          req,
          "vectorSimilarity(BYTE, EUCLIDEAN, bar_i, [4,5,6])",
          "vectorSimilarity(BYTE, EUCLIDEAN, field(bar_i), [4, 5,  6])",
          "vectorSimilarity(BYTE, EUCLIDEAN,$f1, [4, 5,  6])",
          "vectorSimilarity(BYTE, EUCLIDEAN, $f1, [4, 5,  6])",
          "vectorSimilarity(BYTE, EUCLIDEAN, $f2, [4, 5,  6])",
          "vectorSimilarity(BYTE, EUCLIDEAN, $f3, [4, 5,  6])");
    }

    try (SolrQueryRequest req =
        req(
            "f", "vector",
            "v1", "[1,2,3,4]",
            "v2", " [1, 2, 3, 4]")) {
      assertFuncEquals(
          req,
          "vectorSimilarity(FLOAT32,COSINE,vector,[1,2,3,4])",
          "vectorSimilarity(FLOAT32,COSINE,vector,$v1)",
          "vectorSimilarity(FLOAT32,COSINE,vector, $v1)",
          "vectorSimilarity(FLOAT32,COSINE,vector,$v2)",
          "vectorSimilarity(FLOAT32,COSINE,vector, $v2)",
          "vectorSimilarity(vector,[1,2,3,4])",
          "vectorSimilarity( vector,[1,2,3,4])",
          "vectorSimilarity( $f,[1,2,3,4])",
          "vectorSimilarity(vector,$v1)",
          "vectorSimilarity(vector, $v1)",
          "vectorSimilarity( $f, $v1)",
          "vectorSimilarity(vector,$v2)",
          "vectorSimilarity(vector, $v2)");
    }

    try (SolrQueryRequest req =
        req(
            "f", "vector_byte",
            "v1", "[1,2,3,4]",
            "v2", " [1, 2, 3, 4]")) {
      assertFuncEquals(
          req,
          "vectorSimilarity(BYTE,COSINE,vector_byte,[1,2,3,4])",
          "vectorSimilarity(BYTE,COSINE,vector_byte,$v1)",
          "vectorSimilarity(BYTE,COSINE,vector_byte, $v1)",
          "vectorSimilarity(BYTE,COSINE,vector_byte,$v2)",
          "vectorSimilarity(BYTE,COSINE,vector_byte, $v2)",
          "vectorSimilarity(vector_byte,[1,2,3,4])",
          "vectorSimilarity( vector_byte,[1,2,3,4])",
          "vectorSimilarity( $f,[1,2,3,4])",
          "vectorSimilarity(vector_byte,$v1)",
          "vectorSimilarity(vector_byte, $v1)",
          "vectorSimilarity( $f, $v1)",
          "vectorSimilarity(vector_byte,$v2)",
          "vectorSimilarity(vector_byte, $v2)");
    }

    // contrived, but helps us test the param resolution
    // for both field names in the 2arg usecase
    try (SolrQueryRequest req = req("f", "vector")) {
      assertFuncEquals(
          req,
          "vectorSimilarity($f, $f)",
          "vectorSimilarity($f, vector)",
          "vectorSimilarity(vector, $f)",
          "vectorSimilarity(vector, vector)");
    }
  }

  public void testFuncQuery() throws Exception {
    SolrQueryRequest req = req("myQ", "asdf");
    try {
      assertFuncEquals(req, "query($myQ)", "query($myQ,0)", "query({!lucene v=$myQ},0)");
    } finally {
      req.close();
    }
  }

  public void testFuncBoost() throws Exception {
    SolrQueryRequest req = req("myQ", "asdf");
    try {
      assertFuncEquals(req, "boost($myQ,sum(4,5))", "boost({!lucene v=$myQ},sum(4,5))");
    } finally {
      req.close();
    }
  }

  public void testFuncJoindf() throws Exception {
    assertFuncEquals("joindf(foo,bar)");
  }

  public void testFuncGeodist() throws Exception {
    String pt = "10.312,-20.556";
    try (SolrQueryRequest req = req("pt", pt, "sfield", "store")) {

      assertFuncEquals(
          req, "geodist($pt)", "geodist(" + pt + ")", "geodist(" + pt + "," + pt + ")");

      assertFuncEquals(req, "geodist()");
      // geodist() does not support field names in its arguments sometimes
      //               "geodist(store,$pt)",
      //               "geodist(field(store),$pt)",
    }
  }

  public void testFuncHsin() throws Exception {
    assertFuncEquals("hsin(45,true,0,0,45,45)");
  }

  public void testFuncGhhsin() throws Exception {
    assertFuncEquals(
        "ghhsin(45,id,'asdf')",
        "ghhsin(45,field(id),'asdf')"); // "id" is just a single-valued string field
  }

  public void testFuncGeohash() throws Exception {
    assertFuncEquals("geohash(45,99)");
  }

  public void testFuncDist() throws Exception {
    assertFuncEquals("dist(2,45,99,101,111)", "dist(2,vector(45,99),vector(101,111))");
  }

  public void testFuncSqedist() throws Exception {
    assertFuncEquals("sqedist(45,99,101,111)", "sqedist(vector(45,99),vector(101,111))");
  }

  public void testFuncMin() throws Exception {
    assertFuncEquals("min(5,4,3,2,1)", "min(5, 4, 3, 2, 1)");
    assertFuncEquals("min(foo_i,4)", "min(field('foo_i'), 4)");
    assertFuncEquals("min(foo_i,sub(4,field('bar_i')))", "min(field(foo_i), sub(4,bar_i))");
  }

  public void testFuncMax() throws Exception {
    assertFuncEquals("max(5,4,3,2,1)", "max(5, 4, 3, 2, 1)");
    assertFuncEquals("max(foo_i,4)", "max(field('foo_i'), 4)");
    assertFuncEquals("max(foo_i,sub(4,field('bar_i')))", "max(field(foo_i), sub(4,bar_i))");
  }

  public void testFuncMs() throws Exception {
    // Note ms() takes in field name, not field(...)
    assertFuncEquals("ms()", "ms(NOW)");
    assertFuncEquals("ms(2000-01-01T00:00:00Z)", "ms('2000-01-01T00:00:00Z')");
    assertFuncEquals("ms(myDateField_dt)", "ms('myDateField_dt')");
    assertFuncEquals(
        "ms(2000-01-01T00:00:00Z,myDateField_dt)", "ms('2000-01-01T00:00:00Z','myDateField_dt')");
    assertFuncEquals("ms(myDateField_dt, NOW)", "ms('myDateField_dt', NOW)");
  }

  public void testFuncMathConsts() throws Exception {
    assertFuncEquals("pi()");
    assertFuncEquals("e()");
  }

  public void testFuncTerms() throws Exception {
    SolrQueryRequest req = req("myField", "field_t", "myTerm", "my term");
    try {
      for (final String type :
          new String[] {
            "docfreq", "termfreq",
            "totaltermfreq", "ttf",
            "idf", "tf"
          }) {
        // NOTE: these functions takes a field *name* not a field(..) source
        assertFuncEquals(
            req,
            type + "('field_t','my term')",
            type + "(field_t,'my term')",
            type + "(field_t,$myTerm)",
            type + "(field_t,$myTerm)",
            type + "($myField,$myTerm)");
      }

      // ttf is an alias for totaltermfreq
      assertFuncEquals(
          req,
          "ttf(field_t,'my term')",
          "ttf('field_t','my term')",
          "totaltermfreq(field_t,'my term')");

    } finally {
      req.close();
    }
  }

  public void testFuncSttf() throws Exception {
    // sttf is an alias for sumtotaltermfreq
    assertFuncEquals(
        "sttf(foo_t)", "sttf('foo_t')",
        "sumtotaltermfreq(foo_t)", "sumtotaltermfreq('foo_t')");
    assertFuncEquals("sumtotaltermfreq('foo_t')");
  }

  public void testFuncNorm() throws Exception {
    assertFuncEquals("norm(foo_t)", "norm('foo_t')");
  }

  public void testFuncMaxdoc() throws Exception {
    assertFuncEquals("maxdoc()");
  }

  public void testFuncNumdocs() throws Exception {
    assertFuncEquals("numdocs()");
  }

  public void testFuncBools() throws Exception {
    SolrQueryRequest req = req("myTrue", "true", "myFalse", "false");
    try {
      assertFuncEquals(req, "true", "$myTrue");
      assertFuncEquals(req, "false", "$myFalse");
    } finally {
      req.close();
    }
  }

  public void testFuncExists() throws Exception {
    SolrQueryRequest req = req("myField", "field_t", "myQ", "asdf");
    try {
      assertFuncEquals(
          req,
          "exists(field_t)",
          "exists($myField)",
          "exists(field('field_t'))",
          "exists(field($myField))");
      assertFuncEquals(req, "exists(query($myQ))", "exists(query({!lucene v=$myQ}))");
    } finally {
      req.close();
    }
  }

  public void testFuncIsnan() throws Exception {
    SolrQueryRequest req = req("num", "12.3456", "zero", "0");
    try {
      assertFuncEquals(req, "isnan(12.3456)", "isnan(12.3456)", "isnan($num)");
      assertFuncEquals(req, "isnan(div(0,0))", "isnan(div($zero,$zero))");
    } finally {
      req.close();
    }
  }

  public void testFuncNot() throws Exception {
    SolrQueryRequest req = req("myField", "field_b", "myTrue", "true");
    try {
      assertFuncEquals(req, "not(true)", "not($myTrue)");
      assertFuncEquals(req, "not(not(true))", "not(not($myTrue))");
      assertFuncEquals(
          req, "not(field_b)", "not($myField)", "not(field('field_b'))", "not(field($myField))");
      assertFuncEquals(
          req,
          "not(exists(field_b))",
          "not(exists($myField))",
          "not(exists(field('field_b')))",
          "not(exists(field($myField)))");

    } finally {
      req.close();
    }
  }

  public void testFuncDoubleValueBools() throws Exception {
    SolrQueryRequest req = req("myField", "field_b", "myTrue", "true");
    try {
      for (final String type : new String[] {"and", "or", "xor"}) {
        assertFuncEquals(
            req,
            type + "(field_b,true)",
            type + "(field_b,$myTrue)",
            type + "(field('field_b'),true)",
            type + "(field($myField),$myTrue)",
            type + "($myField,$myTrue)");
      }
    } finally {
      req.close();
    }
  }

  public void testFuncIf() throws Exception {
    SolrQueryRequest req =
        req(
            "myBoolField", "foo_b",
            "myIntField", "bar_i",
            "myTrue", "true");
    try {
      assertFuncEquals(
          req,
          "if(foo_b,bar_i,25)",
          "if($myBoolField,bar_i,25)",
          "if(field('foo_b'),$myIntField,25)",
          "if(field($myBoolField),field('bar_i'),25)");
      assertFuncEquals(req, "if(true,37,field($myIntField))", "if($myTrue,37,$myIntField)");
    } finally {
      req.close();
    }
  }

  public void testFuncDef() throws Exception {
    SolrQueryRequest req = req("myField", "bar_f");

    try {
      assertFuncEquals(req, "def(bar_f,25)", "def($myField,25)", "def(field('bar_f'),25)");
      assertFuncEquals(
          req, "def(ceil(bar_f),25)", "def(ceil($myField),25)", "def(ceil(field('bar_f')),25)");
    } finally {
      req.close();
    }
  }

  public void testFuncConcat() throws Exception {
    SolrQueryRequest req = req("myField", "bar_f", "myOtherField", "bar_t");

    try {
      assertFuncEquals(
          req,
          "concat(bar_f,bar_t)",
          "concat($myField,bar_t)",
          "concat(bar_f,$myOtherField)",
          "concat($myField,$myOtherField)");

    } finally {
      req.close();
    }
  }

  public void testFuncSingleValueMathFuncs() throws Exception {
    SolrQueryRequest req = req("myVal", "45", "myField", "foo_i");
    for (final String func :
        new String[] {
          "abs", "rad", "deg", "sqrt", "cbrt", "log", "ln", "exp", "sin", "cos", "tan", "asin",
          "acos", "atan", "sinh", "cosh", "tanh", "ceil", "floor", "rint"
        }) {
      try {
        assertFuncEquals(req, func + "(field(foo_i))", func + "(foo_i)", func + "($myField)");
        assertFuncEquals(req, func + "(45)", func + "($myVal)");
      } finally {
        req.close();
      }
    }
  }

  public void testFuncDoubleValueMathFuncs() throws Exception {
    SolrQueryRequest req = req("myVal", "45", "myOtherVal", "27", "myField", "foo_i");
    for (final String func : new String[] {"pow", "hypot", "atan2"}) {
      try {
        assertFuncEquals(
            req, func + "(field(foo_i),$myVal)", func + "(foo_i,$myVal)", func + "($myField,45)");
        assertFuncEquals(
            req, func + "(45,$myOtherVal)", func + "($myVal,27)", func + "($myVal,$myOtherVal)");

      } finally {
        req.close();
      }
    }
  }

  public void testFuncStrdist() throws Exception {
    SolrQueryRequest req = req("myVal", "zot", "myOtherVal", "yak", "myField", "foo_s1");
    try {
      assertFuncEquals(
          req,
          "strdist(\"zot\",literal('yak'),edit)",
          "strdist(literal(\"zot\"),'yak',   edit  )",
          "strdist(literal($myVal),literal($myOtherVal),edit)");
      assertFuncEquals(
          req, "strdist(\"zot\",literal($myOtherVal),ngram)", "strdist(\"zot\",'yak', ngram, 2)");
      assertFuncEquals(
          req,
          "strdist(field('foo_s1'),literal($myOtherVal),jw)",
          "strdist(field($myField),\"yak\",jw)",
          "strdist($myField,'yak', jw)");
    } finally {
      req.close();
    }
  }

  public void testFuncField() throws Exception {
    assertFuncEquals("field(\"foo_i\")", "field('foo_i\')", "foo_i");

    // simple VS of single valued field should be same as asking for min/max on that field
    assertFuncEquals(
        "field(\"foo_i\")",
        "field('foo_i',min)",
        "field(foo_i,'min')",
        "field('foo_i',max)",
        "field(foo_i,'max')",
        "foo_i");

    // multivalued field with selector
    String multif = "multi_int_with_docvals";
    SolrQueryRequest req = req("my_field", multif);
    // this test is only viable if it's a multivalued field, sanity check the schema
    assertTrue(
        multif + " is no longer multivalued, who broke this schema?",
        req.getSchema().getField(multif).multiValued());
    assertFuncEquals(req, "field($my_field,'MIN')", "field('" + multif + "',min)");
    assertFuncEquals(req, "field($my_field,'max')", "field('" + multif + "',Max)");
  }

  public void testFuncCurrency() throws Exception {
    assertFuncEquals(
        "currency(\"amount\")",
        "currency('amount\')",
        "currency(amount)",
        "currency(amount,USD)",
        "currency('amount',USD)");
  }

  public void testFuncRelatedness() throws Exception {
    SolrQueryRequest req = req("fore", "foo_s:front", "back", "foo_s:back");
    try {
      assertFuncEquals(
          req,
          "agg_relatedness({!query v='foo_s:front'}, {!query v='foo_s:back'})",
          "agg_relatedness($fore, $back)");
    } finally {
      req.close();
    }
  }

  public void testTestFuncs() throws Exception {
    assertFuncEquals("sleep(1,5)", "sleep(1,5)");
    assertFuncEquals("threadid()", "threadid()");
  }

  // TODO: more tests
  public void testQueryMaxScore() throws Exception {
    assertQueryEquals("maxscore", "{!maxscore}A OR B OR C", "A OR B OR C");
    assertQueryEquals("maxscore", "{!maxscore}A AND B", "A AND B");
    assertQueryEquals("maxscore", "{!maxscore}apache -solr", "apache  -solr", "apache -solr ");
    assertQueryEquals("maxscore", "+apache +solr", "apache AND solr", "+apache +solr");
  }

  /**
   * this test does not assert anything itself, it simply toggles a static boolean informing
   * an @AfterClass method to assert that every default qparser and valuesource parser configured
   * was recorded by assertQueryEquals and assertFuncEquals.
   */
  public void testParserCoverage() {
    doAssertParserCoverage = true;
  }

  public void testQuerySimple() throws Exception {
    SolrQueryRequest req = req("myField", "foo_s");
    try {
      assertQueryEquals(
          "simple",
          req,
          "{!simple f=$myField}asdf",
          "{!simple f=$myField v=asdf}",
          "{!simple f=foo_s}asdf");
    } finally {
      req.close();
    }
  }

  public void testQueryMLT() throws Exception {
    assertU(adoc("id", "1", "lowerfilt", "sample data"));
    assertU(commit());
    try {
      assertQueryEquals("mlt", "{!mlt qf=lowerfilt}1", "{!mlt qf=lowerfilt v=1}");
    } finally {
      delQ("*:*");
      assertU(commit());
    }
  }

  public void testQueryMLTContent() throws Exception {
    assertU(adoc("id", "1", "lowerfilt", "sample data", "standardfilt", "sample data"));
    assertU(commit());
    try {
      assertQueryEquals(
          "mlt_content",
          "{!mlt_content qf=lowerfilt mindf=0 mintf=0}sample data",
          "{!mlt_content qf=lowerfilt mindf=0 mintf=0 v='sample data'}",
          "{!qf=lowerfilt mindf=0 mintf=0}sample data");
      SolrQueryRequest req = req(new String[] {"df", "text"});
      try {
        QueryUtils.checkUnequal(
            QParser.getParser("{!mlt_content qf=lowerfilt mindf=0 mintf=0}sample data", req)
                .getQuery(),
            QParser.getParser(
                    "{!mlt_content qf=lowerfilt qf=standardfilt mindf=0 mintf=0}sample data", req)
                .getQuery());
      } finally {
        req.close();
      }

    } finally {
      delQ("*:*");
      assertU(commit());
    }
  }

  public void testQueryKNN() throws Exception {
    SolrInputDocument doc = new SolrInputDocument();
    doc.addField("id", "0");
    doc.addField("vector", Arrays.asList(1, 2, 3, 4));
    assertU(adoc(doc));
    assertU(commit());

    final String qvec = "[1.0,2.0,3.0,4.0]";

    try (SolrQueryRequest req0 = req()) {

      // no filters
      final Query fqNull =
          assertQueryEqualsAndReturn(
              "knn",
              req0,
              "{!knn f=vector}" + qvec,
              "{!knn f=vector preFilter=''}" + qvec,
              "{!knn f=vector v=" + qvec + "}");

      try (SolrQueryRequest req1 = req("fq", "{!tag=t1}id:1", "xxx", "id:1")) {
        // either global fq, or (same) preFilter as localparam
        final Query fqOne =
            assertQueryEqualsAndReturn(
                "knn",
                req1,
                "{!knn f=vector}" + qvec,
                "{!knn f=vector includeTags=t1}" + qvec,
                "{!knn f=vector preFilter='id:1'}" + qvec,
                "{!knn f=vector preFilter=$xxx}" + qvec,
                "{!knn f=vector v=" + qvec + "}");
        QueryUtils.checkUnequal(fqNull, fqOne);

        try (SolrQueryRequest req2 = req("fq", "{!tag=t2}id:2", "xxx", "id:1", "yyy", "")) {
          // override global fq with local param to use different preFilter
          final Query fqOneOverride =
              assertQueryEqualsAndReturn(
                  "knn",
                  req2,
                  "{!knn f=vector preFilter='id:1'}" + qvec,
                  "{!knn f=vector preFilter=$xxx}" + qvec);
          QueryUtils.checkEqual(fqOne, fqOneOverride);

          // override global fq with local param to use no preFilters
          final Query fqNullOverride =
              assertQueryEqualsAndReturn(
                  "knn",
                  req2,
                  "{!knn f=vector preFilter=''}" + qvec,
                  "{!knn f=vector excludeTags=t2}" + qvec,
                  "{!knn f=vector preFilter=$yyy}" + qvec);
          QueryUtils.checkEqual(fqNull, fqNullOverride);
        }
      }

      try (SolrQueryRequest reqPostFilter = req("fq", "{!tag=post frange cache=false l=0}9.9")) {
        // global post-filter fq should always be ignored
        final Query fqPostFilter =
            assertQueryEqualsAndReturn(
                "knn",
                reqPostFilter,
                "{!knn f=vector}" + qvec,
                "{!knn f=vector includeTags=post}" + qvec);
        QueryUtils.checkEqual(fqNull, fqPostFilter);
      }

    } finally {
      delQ("id:0");
      assertU(commit());
    }
  }

  public void testQueryVecSim() throws Exception {
    SolrInputDocument doc = new SolrInputDocument();
    doc.addField("id", "0");
    doc.addField("vector", Arrays.asList(1, 2, 3, 4));
    assertU(adoc(doc));
    assertU(commit());

    final String common = "!vectorSimilarity minReturn=0.3 f=vector";
    final String qvec = "[1.0,2.0,3.0,4.0]";

    try (SolrQueryRequest req0 = req()) {

      // no filters
      final Query fqNull =
          assertQueryEqualsAndReturn(
              "vectorSimilarity",
              req0,
              "{" + common + "}" + qvec,
              "{" + common + " minTraverse='-Infinity'}" + qvec,
              "{" + common + " preFilter=''}" + qvec,
              "{" + common + " v=" + qvec + "}");

      try (SolrQueryRequest req1 = req("fq", "{!tag=t1}id:1", "xxx", "id:1")) {
        // either global fq, or (same) preFilter as localparam
        final Query fqOne =
            assertQueryEqualsAndReturn(
                "vectorSimilarity",
                req1,
                "{" + common + "}" + qvec,
                "{" + common + " includeTags=t1}" + qvec,
                "{" + common + " preFilter='id:1'}" + qvec,
                "{" + common + " preFilter=$xxx}" + qvec,
                "{" + common + " v=" + qvec + "}");
        QueryUtils.checkUnequal(fqNull, fqOne);

        try (SolrQueryRequest req2 = req("fq", "{!tag=t2}id:2", "xxx", "id:1", "yyy", "")) {
          // override global fq with local param to use different preFilter
          final Query fqOneOverride =
              assertQueryEqualsAndReturn(
                  "vectorSimilarity",
                  req2,
                  "{" + common + " preFilter='id:1'}" + qvec,
                  "{" + common + " preFilter=$xxx}" + qvec);
          QueryUtils.checkEqual(fqOne, fqOneOverride);

          // override global fq with local param to use no preFilters
          final Query fqNullOverride =
              assertQueryEqualsAndReturn(
                  "vectorSimilarity",
                  req2,
                  "{" + common + " preFilter=''}" + qvec,
                  "{" + common + " excludeTags=t2}" + qvec,
                  "{" + common + " preFilter=$yyy}" + qvec);
          QueryUtils.checkEqual(fqNull, fqNullOverride);
        }
      }

      try (SolrQueryRequest reqPostFilter = req("fq", "{!tag=post frange cache=false l=0}9.9")) {
        // global post-filter fq should always be ignored
        final Query fqPostFilter =
            assertQueryEqualsAndReturn(
                "vectorSimilarity",
                reqPostFilter,
                "{" + common + "}" + qvec,
                "{" + common + " includeTags=post}" + qvec);
        QueryUtils.checkEqual(fqNull, fqPostFilter);
      }

    } finally {
      delQ("id:0");
      assertU(commit());
    }
  }

  /**
   * NOTE: defType is not only used to pick the parser, but also to record the parser being tested
   * for coverage sanity checking
   *
   * @see #testParserCoverage
   * @see #assertQueryEqualsAndReturn
   */
  protected void assertQueryEquals(final String defType, final String... inputs) throws Exception {
    SolrQueryRequest req = req(new String[] {"df", "text"});
    try {
      assertQueryEqualsAndReturn(defType, req, inputs);
    } finally {
      req.close();
    }
  }

  /**
   * NOTE: defType is not only used to pick the parser, but, if non-null it is also to record the
   * parser being tested for coverage sanity checking
   *
   * @see #testParserCoverage
   * @see #assertQueryEqualsAndReturn
   */
  protected void assertQueryEquals(
      final String defType, final SolrQueryRequest req, final String... inputs) throws Exception {
    assertQueryEqualsAndReturn(defType, req, inputs);
  }

  /**
   * Parses a set of input strings in the context of a request, making various assertions about the
   * resulting Query objects, including that they must all be equals.
   *
   * <p>Returns one of the (all equal) Query objects so it may be used in other comparisons with
   * other Query objects, possibly parsed in the context of different requests.
   *
   * <p>NOTE: defType is not only used to pick the parser, but, if non-null it is also to record the
   * parser being tested for coverage sanity checking.
   *
   * @see QueryUtils#check
   * @see QueryUtils#checkEqual
   * @see #testParserCoverage
   */
  protected Query assertQueryEqualsAndReturn(
      final String defType, final SolrQueryRequest req, final String... inputs) throws Exception {

    assertTrue(
        "At least one input string for parsing must be passed to this method", 0 < inputs.length);

    if (null != defType) qParsersTested.add(defType);

    final Query[] queries = new Query[inputs.length];

    try {
      SolrQueryResponse rsp = new SolrQueryResponse();
      SolrRequestInfo.setRequestInfo(new SolrRequestInfo(req, rsp));
      for (int i = 0; i < inputs.length; i++) {
        queries[i] = QParser.getParser(inputs[i], defType, true, req).getQuery();
      }
    } finally {
      SolrRequestInfo.clearRequestInfo();
    }

    for (Query query1 : queries) {
      QueryUtils.check(query1);
      // yes starting j=0 is redundent, we're making sure every query
      // is equal to itself, and that the quality checks work regardless
      // of which caller/callee is used.
      for (Query query2 : queries) {
        QueryUtils.checkEqual(query1, query2);
      }
    }
    return queries[0];
  }

  /**
   * the function name for val parser coverage checking is extracted from the first input
   *
   * @see #assertQueryEquals
   * @see #testParserCoverage
   */
  protected void assertFuncEquals(final String... inputs) throws Exception {
    SolrQueryRequest req = req();
    try {
      assertFuncEquals(req, inputs);
    } finally {
      req.close();
    }
  }

  /**
   * the function name for val parser coverage checking is extracted from the first input
   *
   * @see #assertQueryEquals
   * @see #testParserCoverage
   */
  protected void assertFuncEquals(final SolrQueryRequest req, final String... inputs)
      throws Exception {
    // pull out the function name
    final String funcName = (new StrParser(inputs[0])).getId();
    valParsersTested.add(funcName);

    assertQueryEquals(FunctionQParserPlugin.NAME, req, inputs);
  }

  public void testAggs() throws Exception {
    assertFuncEquals("agg(avg(foo_i))", "agg(avg(foo_i))");
    assertFuncEquals("agg(avg(foo_i))", "agg_avg(foo_i)");
    assertFuncEquals("agg_min(foo_i)", "agg(min(foo_i))");
    assertFuncEquals("agg_max(foo_i)", "agg(max(foo_i))");

    assertFuncEquals("agg_avg(foo_i)", "agg_avg(foo_i)");
    assertFuncEquals("agg_sum(foo_i)", "agg_sum(foo_i)");
    assertFuncEquals("agg_count()", "agg_count()");
    assertFuncEquals("agg_unique(foo_i)", "agg_unique(foo_i)");
    assertFuncEquals("agg_uniqueBlock(foo_i)", "agg_uniqueBlock(foo_i)");
    assertFuncEquals("agg_hll(foo_i)", "agg_hll(foo_i)");
    assertFuncEquals("agg_sumsq(foo_i)", "agg_sumsq(foo_i)");
    assertFuncEquals("agg_percentile(foo_i,50)", "agg_percentile(foo_i,50)");
    assertFuncEquals("agg_variance(foo_i)", "agg_variance(foo_i)");
    assertFuncEquals("agg_stddev(foo_i)", "agg_stddev(foo_i)");
    assertFuncEquals("agg_missing(foo_i)", "agg_missing(foo_i)");
    assertFuncEquals("agg(missing(foo_i))", "agg(missing(foo_i))");
    assertFuncEquals("agg_missing(field(foo_i))", "agg_missing(field(foo_i))");
    assertFuncEquals("agg_countvals(foo_i)", "agg_countvals(foo_i)");
    assertFuncEquals("agg(countvals(foo_i))", "agg(countvals(foo_i))");
    assertFuncEquals("agg_countvals(field(foo_i))", "agg_countvals(field(foo_i))");
    // assertFuncEquals("agg_multistat(foo_i)", "agg_multistat(foo_i)");
  }

  public void testCompares() throws Exception {
    assertFuncEquals("gt(foo_i,2)", "gt(foo_i, 2)");
    assertFuncEquals("gt(foo_i,2)", "gt(foo_i,2)");
    assertFuncEquals("lt(foo_i,2)", "lt(foo_i,2)");
    assertFuncEquals("lte(foo_i,2)", "lte(foo_i,2)");
    assertFuncEquals("gte(foo_i,2)", "gte(foo_i,2)");
    assertFuncEquals("eq(foo_i,2)", "eq(foo_i,2)");

    expectThrows(
        AssertionError.class,
        "expected error, functions are not equal",
        () -> assertFuncEquals("eq(foo_i,2)", "lt(foo_i,2)"));
  }

  public void testChildField() throws Exception {
    final SolrQueryRequest req = req("q", "{!parent which=type_s1:parent}whatever_s1:foo");
    try {
      assertFuncEquals(req, "childfield(name_s1,$q)", "childfield(name_s1,$q)");
    } finally {
      req.close();
    }
  }

  public void testPayloadScoreQuery() {
    // There was a bug with PayloadScoreQuery's .equals() method that said two queries were equal
    // with different includeSpanScore settings

    expectThrows(
        AssertionError.class,
        "queries should not have been equal",
        () ->
            assertQueryEquals(
                "payload_score",
                "{!payload_score f=foo_dpf v=query func=min includeSpanScore=false}",
                "{!payload_score f=foo_dpf v=query func=min includeSpanScore=true}"));
  }

  public void testPayloadCheckQuery() {
    expectThrows(
        AssertionError.class,
        "queries should not have been equal",
        () ->
            assertQueryEquals(
                "payload_check",
                "{!payload_check f=foo_dpf payloads=2}one",
                "{!payload_check f=foo_dpf payloads=2}two"));
  }

  public void testPayloadFunction() throws Exception {
    SolrQueryRequest req = req("myField", "bar_f");

    try {
      assertFuncEquals(req, "payload(foo_dpf,some_term)", "payload(foo_dpf,some_term)");
    } finally {
      req.close();
    }
  }

  public void testBoolMmQuery() throws Exception {
    assertQueryEquals(
        "lucene",
        "{!bool should=foo_s:a should=foo_s:b}",
        "{!bool should=foo_s:a should=foo_s:b mm=0}");
    assertQueryEquals(
        "lucene",
        "{!bool should=foo_s:a should=foo_s:b mm=1}",
        "{!bool should=foo_s:a should=foo_s:b mm=1}");
    expectThrows(
        AssertionError.class,
        "queries should not have been equal",
        () ->
            assertQueryEquals(
                "lucene",
                "{!bool should=foo_s:a should=foo_s:b mm=1}",
                "{!bool should=foo_s:a should=foo_s:b}"));
  }

  public void testBoolQuery() throws Exception {
    assertQueryEquals(
        "bool",
        "{!bool must='{!lucene}foo_s:a' must='{!lucene}foo_s:b'}",
        "{!bool must='{!lucene}foo_s:b' must='{!lucene}foo_s:a'}");
    assertQueryEquals(
        "bool",
        "{!bool must_not='{!lucene}foo_s:a' should='{!lucene}foo_s:b' "
            + "must='{!lucene}foo_s:c' filter='{!lucene}foo_s:d' filter='{!lucene}foo_s:e'}",
        "{!bool must='{!lucene}foo_s:c' filter='{!lucene}foo_s:d' "
            + "must_not='{!lucene}foo_s:a' should='{!lucene}foo_s:b' filter='{!lucene}foo_s:e'}");

    expectThrows(
        AssertionError.class,
        "queries should not have been equal",
        () ->
            assertQueryEquals(
                "bool", "{!bool must='{!lucene}foo_s:a'}", "{!bool should='{!lucene}foo_s:a'}"));
  }

  public void testHashRangeQuery() throws Exception {
    assertQueryEquals(
        "hash_range",
        "{!hash_range f=x_id l=107347968 u=214695935}",
        "{!hash_range l='107347968' u='214695935' f='x_id'}");
  }

  // Override req to add df param
  public static SolrQueryRequest req(String... q) {
    return SolrTestCaseJ4.req(q, "df", "text");
  }
}
